﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using XSerialization.Templates;

namespace XSerialization.Generator
{
    public class XGenerator : IXGenerator
    {
        private readonly IList<Tuple<IXsdDocument, IFileMetaInfo, IFileMetaInfo>> _fileInformations;

        /// <summary>
        /// default constructor
        /// </summary>
        public XGenerator()
        {
            _fileInformations = new List<Tuple<IXsdDocument, IFileMetaInfo, IFileMetaInfo>>();
        }

        /// <summary>
        /// Adds the specified file to the generator, using the specified meta informations about code and interface.
        /// </summary>
        /// <param name="xDocument">XDocument object</param>
        /// <param name="interfaceFile">Interface meta informations</param>
        /// <param name="codeFile">Code meta informations</param>
        public void AddFile(XDocument xDocument, IFileMetaInfo interfaceFile, IFileMetaInfo codeFile)
        {
            _fileInformations.Add(new Tuple<IXsdDocument, IFileMetaInfo, IFileMetaInfo>(new XsdDocument(xDocument), interfaceFile, codeFile));
        }

        /// <summary>
        /// Adds the specified file to the generator, using the specified meta informations about code and interface.
        /// </summary>
        /// <param name="xsdFile">File path</param>
        /// <param name="interfaceFile">Interface meta informations</param>
        /// <param name="codeFile">Code meta informations</param>
        public void AddFile(string xsdFile, IFileMetaInfo interfaceFile, IFileMetaInfo codeFile)
        {
            if (string.IsNullOrEmpty(xsdFile)) throw new ArgumentNullException("xsdFile");
            if (interfaceFile == null) throw new ArgumentNullException("interfaceFile");
            if (codeFile == null) throw new ArgumentNullException("codeFile");

            FileInfo xsdFileInfo = new FileInfo(xsdFile);
            if (!xsdFileInfo.Exists) throw new FileNotFoundException("Cannot find the specified xsd file.");

            XDocument xDocument = XDocument.Load(xsdFileInfo.FullName);
            AddFile(xDocument, interfaceFile, codeFile);
        }

        /// <summary>
        /// Start generation
        /// </summary>
        public void Start()
        {
            if (_fileInformations.Count == 0) return;

            //read Xsd elements
            ReadXsdElements();

            //generate code from xsd elements
            CreateTemplates();
        }

        /// <summary>
        /// Saves all genereated files to the specified location.
        /// </summary>
        public void Save()
        {
            foreach (var file in _fileInformations)
            {
                System.Reflection.Assembly ass = System.Reflection.Assembly.GetExecutingAssembly();
                FileInfo assFile = new FileInfo(ass.Location);

                WriteFile(file.Item2, file.Item1.InterfaceCode, assFile.DirectoryName);
                WriteFile(file.Item3, file.Item1.ClassCode, assFile.DirectoryName);
            }
        }

        private void WriteFile(IFileMetaInfo metaInfo, string code, string defaultDir)
        {
            string codeFileDirectory = metaInfo.Directory;
            if (string.IsNullOrEmpty(codeFileDirectory)) codeFileDirectory = defaultDir;

            FileInfo codeFile = new FileInfo(Path.Combine(codeFileDirectory, metaInfo.Name));
            if (codeFile.Exists) codeFile.Delete();

            StreamWriter fileWriter = new StreamWriter(codeFile.FullName);
            fileWriter.WriteLine(code);
            fileWriter.Flush();
            fileWriter.Close();
        }

        private void CreateTemplates()
        {
            foreach (var file in _fileInformations)
            {
                InterfaceFileTemplate interfaceCode = new InterfaceFileTemplate(file.Item1.ComplexTypes.Values.ToList(), file.Item1.EnumerationTypes.Values.ToList(), file.Item2);
                file.Item1.InterfaceCode = interfaceCode.TransformText();

                ClassFileTemplate classCode = new ClassFileTemplate(file.Item1.ComplexTypes.Values.ToList(), file.Item3, file.Item2);
                file.Item1.ClassCode = classCode.TransformText();
            }
        }

        private void ReadXsdElements()
        {
            //read basic informations
            foreach (var file in _fileInformations)
            {
                //Import Xsd NameSpaces -> needed for XElement names
                foreach (XAttribute attribute in file.Item1.XsdFile.Root.Attributes())
                {
                    file.Item1.NameSpaces.Add(attribute.Name.LocalName, attribute.Value);
                }

                foreach (XElement element in file.Item1.XsdFile.Root.Elements())
                {
                    if (element.Name.LocalName.ToLower() == "include") continue;

                    CreateXsdType(file.Item1, element, GetElementComments(element));
                }
            }

            //read detail informations
            ReadAllComplexTypeDetails();
        }

        private string CreateXsdType(IXsdDocument document, XElement element, string comment)
        {
            if (element == null) return null;

            switch (element.Name.LocalName.ToLower())
            {
                case "simpletype":
                {
                    return CreateSimpleType(document, element);
                }
                case "element":
                {
                    string typeAttributeValue = string.Empty;
                    XAttribute typeAttribute = element.Attribute("type");
                    if (typeAttribute != null) typeAttributeValue = typeAttribute.Value;
                    if (string.IsNullOrEmpty(typeAttributeValue))
                    {
                        return CreateComplexType(document, element, comment);
                    }
                    //ignore <xs:element name="sample" type="T_SampleType"/> because external type def is used for code generation
                    return null;
                }
                case "complextype":
                {
                    return CreateComplexType(document, element, comment);
                }
            }

            return null;
        }

        private string CreateSimpleType(IXsdDocument document, XElement element)
        {
            if (document == null || element == null) return null;

            XsdSimpleType newType = new XsdSimpleType(element.Attribute("name").Value, element);

            // restriction declaration
            XElement restriction = element.Element("{" + document.NameSpaces["xs"] + "}restriction");
            if (restriction != null)
            {
                //base xsd Type
                string xsdTypeName = restriction.Attribute("base").Value;
                if (xsdTypeName.StartsWith("xs:")) newType.TypeName = ConvertUtilities.ConvertSimpleTypes(xsdTypeName, false);

                //check enum declaration
                foreach (XElement child in restriction.Elements())
                {
                    if (child.Name.LocalName != "enumeration") continue;
                    if (!document.EnumerationTypes.ContainsKey(newType.XmlName))
                    {
                        document.EnumerationTypes.Add(newType.XmlName, new XsdEnumerationType(newType.Name + "Enum", restriction));
                    }

                    document.EnumerationTypes[newType.XmlName].Enumerations.Add(child.Attribute("value").Value);
                }
            }

            // union declaration
            XElement union = element.Element("{" + document.NameSpaces["xs"] + "}union");
            if (union != null)
            {
                string memberTypes = union.Attribute("memberTypes").Value;
                string[] members = memberTypes.Split(' ');

                foreach (string member in members)
                {
                    if (!document.SimpleTypes.ContainsKey(member)) continue;
                    newType.TypeName = document.SimpleTypes[member].TypeName;
                    break;
                }
            }

            document.SimpleTypes.Add(newType.XmlName, newType);
            return newType.XmlName;
        }

        private string CreateComplexType(IXsdDocument document, XElement element, string comment)
        {
            if (document == null || element == null) return null;

            XsdComplexType newType = new XsdComplexType(element.Attribute("name").Value, element, comment);
            document.ComplexTypes.Add(newType.XmlName, newType);
            if (newType.XsdNode.Name.LocalName.ToLower() != "element") return newType.XmlName;

            //inner typ definition
            if (newType.XsdNode.HasElements)
            {
                newType.XsdNode = element.Element("{" + document.NameSpaces["xs"] + "}complexType");
                return newType.XmlName;
            }

            //external typ definition
            XAttribute typeName = newType.XsdNode.Attribute("type");
            if (document.ComplexTypes.ContainsKey(typeName.Value))
            {
                document.ComplexTypes[typeName.Value].Name = newType.Name;
                return newType.XmlName;
            }

            return newType.XmlName;
        }

        private void ReadAllComplexTypeDetails()
        {
            foreach (var file in _fileInformations)
            {
                //create foreach list because inner types might be added at runtime
                IList<XsdComplexType> complexTypes = file.Item1.ComplexTypes.Values.ToList();

                foreach (XsdComplexType xsdComplexType in complexTypes)
                {
                    ReadComplexTypeDetails(file.Item1, xsdComplexType);
                }
            }
        }

        private void ReadComplexTypeDetails(IXsdDocument document, XsdComplexType xsdComplexType)
        {
            if (document == null || xsdComplexType == null) return;
            int attributeIndex = 100;

            foreach (XElement element in xsdComplexType.XsdNode.Elements())
            {
                string elementType = element.Name.LocalName.ToLower();

                switch (elementType)
                {
                    case "simplecontent":
                        {
                            //read extension base 
                            XElement extention = element.Element("{" + document.NameSpaces["xs"] + "}extension");
                            IXsdTypeReference content = new XsdTypeReference("Value", false, false, false, 0, GetElementComments(element));
                            content.XmlName = "";
                            GetElementType(document, extention, content);

                            xsdComplexType.Properties.Add(content.XmlName, content);

                            int index = 100;
                            //read attributes
                            foreach (XElement attribute in extention.Elements())
                            {
                                if (attribute.Name.LocalName.ToLower() == "attribute")
                                {
                                    IXsdTypeReference reference = ReadAttributeElement(document, attribute, index);
                                    xsdComplexType.Properties.Add(reference.XmlName, reference);

                                    index = index + 1;
                                }
                            }

                            break;
                        }
                    case "complexcontent":
                        {
                            XElement restriction = element.Element("{" + document.NameSpaces["xs"] + "}restriction");
                            if (restriction != null)
                            {
                                foreach (XElement attribute in restriction.Elements("{" + document.NameSpaces["xs"] + "}attribute"))
                                {
                                    IXsdTypeReference reference = ReadAttributeElement(document, attribute, attributeIndex);
                                    reference.Description = GetElementComments(attribute);
                                    xsdComplexType.Properties.Add(reference.XmlName, reference);

                                    attributeIndex = attributeIndex + 1;
                                }
                            }

                            break;
                        }
                    case "attribute":
                        {
                            IXsdTypeReference reference = ReadAttributeElement(document, element, attributeIndex);
                            reference.Description = GetElementComments(element);
                            xsdComplexType.Properties.Add(reference.XmlName, reference);

                            attributeIndex = attributeIndex + 1;
                            break;
                        }
                    case "choice":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, true, 0);
                            break;
                        }
                    case "sequence":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, false, 0);
                            break;
                        }
                    case "all":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, true, 0);
                            break;
                        }
                }
            }
        }

        private int ReadSequenceElements(IXsdDocument document, XElement sequence, XsdComplexType complexType, bool allOptional, int index)
        {
            foreach (XElement element in sequence.Elements())
            {
                switch (element.Name.LocalName)
                {
                    case "element":
                        {
                            XAttribute name = element.Attribute("name");
                            if (name == null)
                            {
                                name = element.Attribute("ref");
                                if (name == null) continue;
                            }

                            bool isRequired = true;
                            bool isList = false;
                            // check min occurs for IsRequired
                            if (element.Attribute("minOccurs") != null &&
                                Convert.ToUInt32(element.Attribute("minOccurs").Value) == 0) isRequired = false;
                            // check max occurs for IsList 
                            if (element.Attribute("maxOccurs") != null &&
                                (element.Attribute("maxOccurs").Value == "unbounded" ||
                                 Convert.ToUInt32(element.Attribute("maxOccurs").Value) > 1)) isList = true;

                            //"choise" and "all" elements are optional
                            if (allOptional) isRequired = false;

                            XsdTypeReference reference = new XsdTypeReference(name.Value, false, isRequired, isList, index, GetElementComments(element));
                            GetElementType(document, element, reference);

                            complexType.Properties.Add(reference.XmlName, reference);
                            index = index + 1;
                            break;
                        }
                    case "choice":
                        {
                            index = ReadSequenceElements(document, element, complexType, true, index);
                            break;
                        }
                    case "sequence":
                        {
                            index = ReadSequenceElements(document, element, complexType, false, index);
                            break;
                        }
                }
            }

            return index;
        }

        private void SetReferenceType(string type, IXsdTypeReference reference)
        {
            if (type.StartsWith("xs:"))
            {
                reference.TypeInterfaceName = ConvertUtilities.ConvertSimpleTypes(type, reference.IsRequired);
                reference.TypeName = ConvertUtilities.ConvertSimpleTypes(type, reference.IsRequired);
                reference.IsSimpleType = true;
            }
            else
            {
                foreach (var file in _fileInformations)
                {
                    if (file.Item1.ComplexTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = file.Item1.ComplexTypes[type].InterfaceName;
                        reference.TypeName = file.Item1.ComplexTypes[type].Name;
                    }
                    if (file.Item1.SimpleTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = file.Item1.SimpleTypes[type].TypeName;
                        reference.TypeName = file.Item1.SimpleTypes[type].TypeName;
                        reference.IsSimpleType = true;
                    }
                    if (file.Item1.EnumerationTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = file.Item1.EnumerationTypes[type].Name;
                        reference.TypeName = file.Item1.EnumerationTypes[type].Name;
                        reference.IsSimpleType = true;

                        if (!reference.IsRequired)
                        {
                            reference.TypeInterfaceName = reference.TypeInterfaceName + "?";
                            reference.TypeName = reference.TypeName + "?";
                        }
                    }
                }
            }

            //if type not appears in the document list -> insert name/interface from value
            if (string.IsNullOrEmpty(reference.TypeName))
            {
                string typeName = ConvertUtilities.ConvertXsdName(type);
                reference.TypeInterfaceName = "I" + typeName;
                reference.TypeName = typeName;
            }
        }

        private void GetElementType(IXsdDocument document, XElement element, IXsdTypeReference reference)
        {
            XAttribute type = element.Attribute("type");
            reference.IsSimpleType = false;

            if (type == null)
            {
                XAttribute refElement = element.Attribute("ref");
                if (refElement != null)
                {
                    SetReferenceType(refElement.Value, reference);
                }
                else
                {
                    XAttribute baseElement = element.Attribute("base");
                    if (baseElement != null)
                    {
                        SetReferenceType(baseElement.Value, reference);
                    }
                    else
                    {
                        //create new type
                        string newTypeName = CreateXsdType(document, element, GetElementComments(element));
                        if (string.IsNullOrEmpty(newTypeName)) return;

                        if (document.ComplexTypes.ContainsKey(newTypeName))
                        {
                            ReadComplexTypeDetails(document, document.ComplexTypes[newTypeName]);
                        }

                        SetReferenceType(newTypeName, reference);
                    }
                }
            }
            else
            {
                SetReferenceType(type.Value, reference);
            }
        }

        private IXsdTypeReference ReadAttributeElement(IXsdDocument document, XElement element, int index)
        {
            XAttribute name = element.Attribute("name");
            XAttribute use = element.Attribute("use");
            bool isRequired = use != null && use.Value == "required";

            IXsdTypeReference reference = new XsdTypeReference(name.Value, true, isRequired, false, index, GetElementComments(element));
            GetElementType(document, element, reference);

            return reference;
        }

        private string GetElementComments(XElement element)
        {
            if (element == null) return string.Empty;
            if (element.PreviousNode == null) return string.Empty;
            if (element.PreviousNode.GetType() != typeof(XComment)) return string.Empty;

            string description = ((XComment)element.PreviousNode).Value.Trim();
            //try to find comments in the previous node
            if (string.IsNullOrEmpty(description) || string.IsNullOrWhiteSpace(description))
            {
                if (element.PreviousNode.PreviousNode != null && element.PreviousNode.PreviousNode.GetType() == typeof(XComment))
                {
                    description = ((XComment)element.PreviousNode.PreviousNode).Value.Trim();
                }
            }

            //remove forbidden chars
            if (!string.IsNullOrEmpty(description)) return ConvertUtilities.ConvertXsdName(description);

            return string.Empty;
        }
    }
}
